package com.ifonly.common;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 类扫描器
 * <p/>
 * 实现class文件的识别, 可以指定的包下面的class文件, 是否包含jar, 是否是某个类或接口的子类 等功能
 *
 * @author Jerry Ou
 * @version 1.0 2015-11-29 23:33
 * @since JDK 1.6
 */
public class ClassScanner {

    /**
     * 需要搜索的包
     */
    private List<String> packages = new ArrayList<String>();
    /**
     * 需要扫描的父类或接口
     */
    private List<Class<?>> classes = new ArrayList<Class<?>>();
    /**
     * 需要扫描的jar
     */
    private List<String> jars = new ArrayList<String>();

    /**
     * class文件目录
     */
    private String classDir;
    /**
     * jar文件目录
     */
    private String libDir;
    /**
     * 是否扫描全部的jar文件
     */
    private boolean allJars;

    /**
     * 设置 class 文件所在目录
     *
     * @param classDir class 文件所在目录
     * @return ClassScanner实例
     */
    public ClassScanner classDir(String classDir) {
        this.classDir = classDir;
        return this;
    }

    /**
     * 设置 jar 文件所在目录
     *
     * @param libDir jar 文件所在目录
     * @return ClassScanner实例
     */
    public ClassScanner libDir(String libDir) {
        this.libDir = libDir;
        return this;
    }

    /**
     * 是否包含所有 jar 文件
     * 如果设置为 true, 通过 jars 方法设置的 需要扫描的 jar 文件会失效
     *
     * @param allJars 是否包含所有 jar
     * @return ClassScanner实例
     */
    public ClassScanner allJars(boolean allJars) {
        this.allJars = allJars;
        return this;
    }

    /**
     * 私有化构造方法不允许外部创建 ClassScanner 对象的实例
     */
    private ClassScanner() {

    }

    /**
     * 创建 ClassScanner, 并可指定需要搜索的包名
     *
     * @param packages 指定的包
     * @return ClassScanner 实例
     */
    public static ClassScanner of(String... packages) {
        List<String> list = new ArrayList<String>();
        if (packages != null && packages.length > 0) {
            Collections.addAll(list, packages);
        }
        return of(list);
    }

    /**
     * 创建 ClassScanner, 并可指定需要搜索的包名
     *
     * @param packages 指定的包
     * @return ClassScanner 实例
     */
    public static ClassScanner of(List<String> packages) {
        ClassScanner scanner = new ClassScanner();
        if (packages != null && packages.size() > 0) {
            scanner.packages.addAll(packages);
        }
        return scanner;
    }

    /**
     * 需要扫描的是哪些类的子类/接口的实现
     *
     * @param classes 子类/接口
     * @return ClassScanner 实例
     */
    public ClassScanner from(Class<?>... classes) {
        List<Class<?>> list = new ArrayList<Class<?>>();
        if (classes != null && classes.length > 0) {
            Collections.addAll(list, classes);
        }
        return from(list);
    }

    /**
     * 需要扫描的是哪些类的子类/接口的实现
     *
     * @param classes 子类/接口
     * @return ClassScanner 实例
     */
    public ClassScanner from(List<Class<?>> classes) {
        this.classes.addAll(classes);
        return this;
    }

    /**
     * 包含的jar 名称
     *
     * @param jars jar名称
     * @return ClassScanner 实例
     */
    public ClassScanner jars(String... jars) {
        List<String> list = new ArrayList<String>();
        if (jars != null && jars.length > 0) {
            Collections.addAll(list, jars);
        }
        return jars(list);
    }

    /**
     * 包含的jar 名称
     *
     * @param jars jar名称
     * @return ClassScanner 实例
     */
    public ClassScanner jars(List<String> jars) {
        this.jars.addAll(jars);
        return this;
    }

    /**
     * 扫描入口类
     *
     * @return 扫描的结果
     * @throws IOException IO异常
     */
    public List<Class<?>> scan() throws IOException {
        prepare();
        List<Class<?>> results = new ArrayList<Class<?>>();

        if (this.packages.isEmpty()) {
            File classRootFile = new File(this.classDir);
            List<Class<?>> classInClassDir = scanClassDir(classRootFile);
            results.addAll(classInClassDir);
        } else {
            for (String pkg : packages) {
                File classRootFile = new File(this.classDir, pkg.replace(".", File.separator));
                List<Class<?>> classInClassDir = scanClassDir(classRootFile);
                results.addAll(classInClassDir);
            }
        }

        if (!jars.isEmpty()) {
            for (String jar : jars) {
                File libRoot = new File(this.libDir, jar);
                List<Class<?>> classInLibDir = scanLibDir(libRoot);
                results.addAll(classInLibDir);
            }
        }

        return results;
    }

    private List<Class<?>> scanLibDir(File jar) throws IOException {
        List<Class<?>> results = new ArrayList<Class<?>>();

        JarFile jarFile = new JarFile(jar);

        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            String entryName = jarEntry.getName();

            if (this.packages.isEmpty()) {
                if (!jarEntry.isDirectory() && entryName.endsWith(".class")) {
                    String className = entryName
                            .replaceAll(File.separator, ".")
                            .replaceAll("/", ".")
                            .substring(0, entryName.length() - 6);

                    addClassToResult(className, results);
                }
            } else {
                for (String pkg : this.packages) {
                    pkg = pkg.replaceAll("[.]", File.separator);

                    if (!jarEntry.isDirectory() && entryName.endsWith(".class") && entryName.startsWith(pkg)) {
                        String className = entryName
                                .replaceAll(File.separator, ".")
                                .replaceAll("/", ".")
                                .substring(0, entryName.length() - 6);

                        addClassToResult(className, results);

                    }
                }
            }

        }

        return results;
    }

    private List<Class<?>> scanClassDir(File dir) {
        List<Class<?>> results = new ArrayList<Class<?>>();

        File[] listFiles = dir.listFiles(new FileFilter() {
            public boolean accept(File pathname) {
                return pathname.isDirectory() || pathname.getName().endsWith(".class");
            }
        });

        if (listFiles != null) {
            for (File file : listFiles) {
                if (file.isDirectory()) {
                    results.addAll(scanClassDir(file));
                } else {
                    String filePath = file.getAbsolutePath();
                    String classFilePath = filePath.substring(this.classDir.length());
                    String className = classFilePath.substring(0, classFilePath.length() - 6)
                            .replaceAll(File.separator, ".")
                            .replaceAll("/", ".");

                    addClassToResult(className, results);
                }
            }
        }

        return results;
    }

    private void addClassToResult(String className, List<Class<?>> results) {
        try {
            Class<?> clazz = Class.forName(className);
            addClassToResult(clazz, results);
        } catch (NoClassDefFoundError e) {
            //e.printStackTrace();
        } catch (ClassNotFoundException e) {
            //e.printStackTrace();
        } catch (ExceptionInInitializerError e) {
            //e.printStackTrace();
        } catch (Exception e) {
            //e.printStackTrace();
        } catch (Throwable e) {
            //e.printStackTrace();
        }
    }

    private void addClassToResult(Class<?> clazz, List<Class<?>> results) {
        if (filter(clazz)) {
            results.add(clazz);
        }
    }

    /**
     * 准备扫描
     * 检测 classDir 和 libDir
     * 检测 allJars 并且 在 allJars=true 时, 重置 jars
     */
    private void prepare() {
        if (this.classDir == null) {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            URL url = classLoader.getResource("");
            if (url != null) {
                this.classDir = url.getFile();
            } else {
                throw new RuntimeException("init classDir throw error");
            }
        }

        if ((this.allJars && this.libDir == null) || (!this.jars.isEmpty() && this.libDir == null)) {
            throw new RuntimeException("You must set libDir if scan jar files");
        }

        if (this.allJars) {
            this.jars.clear();

            File lib = new File(this.libDir);
            File[] jarFiles = lib.listFiles(new FileFilter() {
                public boolean accept(File file) {
                    return file.getName().endsWith(".jar");
                }
            });

            List<String> jarNames = new ArrayList<String>(jarFiles.length);
            for (File jarFile : jarFiles) {
                jarNames.add(jarFile.getName());
            }
            this.jars.addAll(jarNames);
        }
    }


    @SuppressWarnings("unchecked")
    private boolean filter(Class<?> clazz) {
        if (this.classes == null || this.classes.isEmpty()) {
            return true;
        }

        for (Class clz : this.classes) {
            if (clz.isAssignableFrom(clazz)) {
                return true;
            }
        }
        return false;
    }

    /*
    public static void main(String[] args) {
        try {
            List<Class<?>> list = ClassScanner.of("com", "org.apache")
                    .from(Controller.class, Appender.class)
                    .jars("javax.servlet-api-3.1.0.jar")
                    //.classDir("/Users/jerry/workspace/myProjs/java/jdapper/target/jdapper/WEB-INF/classes")
                    .libDir("/Users/jerry/workspace/myProjs/java/jdapper/target/jdapper/WEB-INF/lib")
                    .allJars(true)
                    .scan();

            for (Class<?> c : list) {
                System.out.println(c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }*/

}

